import {
	Box3,
	Color,
	DoubleSide,
	Frustum,
	Matrix3,
	Matrix4,
	Vector2,
	Vector3,
	Vector4
} from '../three.module.min.js';

class RenderableObject {

	constructor() {

		this.id = 0;

		this.object = null;
		this.z = 0;
		this.renderOrder = 0;

	}

}

//

class RenderableFace {

	constructor() {

		this.id = 0;

		this.v1 = new RenderableVertex();
		this.v2 = new RenderableVertex();
		this.v3 = new RenderableVertex();

		this.normalModel = new Vector3();

		this.vertexNormalsModel = [new Vector3(), new Vector3(), new Vector3()];
		this.vertexNormalsLength = 0;

		this.color = new Color();
		this.material = null;
		this.uvs = [new Vector2(), new Vector2(), new Vector2()];

		this.z = 0;
		this.renderOrder = 0;

	}

}

//

class RenderableVertex {

	constructor() {

		this.position = new Vector3();
		this.positionWorld = new Vector3();
		this.positionScreen = new Vector4();

		this.visible = true;

	}

	copy(vertex) {

		this.positionWorld.copy(vertex.positionWorld);
		this.positionScreen.copy(vertex.positionScreen);

	}

}

//

class RenderableLine {

	constructor() {

		this.id = 0;

		this.v1 = new RenderableVertex();
		this.v2 = new RenderableVertex();

		this.vertexColors = [new Color(), new Color()];
		this.material = null;

		this.z = 0;
		this.renderOrder = 0;

	}

}

//

class RenderableSprite {

	constructor() {

		this.id = 0;

		this.object = null;

		this.x = 0;
		this.y = 0;
		this.z = 0;

		this.rotation = 0;
		this.scale = new Vector2();

		this.material = null;
		this.renderOrder = 0;

	}

}

//

class Projector {

	constructor() {

		let _object, _objectCount, _objectPoolLength = 0,
			_vertex, _vertexCount, _vertexPoolLength = 0,
			_face, _faceCount, _facePoolLength = 0,
			_line, _lineCount, _linePoolLength = 0,
			_sprite, _spriteCount, _spritePoolLength = 0,
			_modelMatrix;

		const

			_renderData = { objects: [], lights: [], elements: [] },

			_vector3 = new Vector3(),
			_vector4 = new Vector4(),

			_clipBox = new Box3(new Vector3(- 1, - 1, - 1), new Vector3(1, 1, 1)),
			_boundingBox = new Box3(),
			_points3 = new Array(3),

			_viewMatrix = new Matrix4(),
			_viewProjectionMatrix = new Matrix4(),

			_modelViewProjectionMatrix = new Matrix4(),

			_frustum = new Frustum(),

			_objectPool = [], _vertexPool = [], _facePool = [], _linePool = [], _spritePool = [];

		//

		function RenderList() {

			const normals = [];
			const colors = [];
			const uvs = [];

			let object = null;

			const normalMatrix = new Matrix3();

			function setObject(value) {

				object = value;

				normalMatrix.getNormalMatrix(object.matrixWorld);

				normals.length = 0;
				colors.length = 0;
				uvs.length = 0;

			}

			function projectVertex(vertex) {

				const position = vertex.position;
				const positionWorld = vertex.positionWorld;
				const positionScreen = vertex.positionScreen;

				positionWorld.copy(position).applyMatrix4(_modelMatrix);
				positionScreen.copy(positionWorld).applyMatrix4(_viewProjectionMatrix);

				const invW = 1 / positionScreen.w;

				positionScreen.x *= invW;
				positionScreen.y *= invW;
				positionScreen.z *= invW;

				vertex.visible = positionScreen.x >= - 1 && positionScreen.x <= 1 &&
					positionScreen.y >= - 1 && positionScreen.y <= 1 &&
					positionScreen.z >= - 1 && positionScreen.z <= 1;

			}

			function pushVertex(x, y, z) {

				_vertex = getNextVertexInPool();
				_vertex.position.set(x, y, z);

				projectVertex(_vertex);

			}

			function pushNormal(x, y, z) {

				normals.push(x, y, z);

			}

			function pushColor(r, g, b) {

				colors.push(r, g, b);

			}

			function pushUv(x, y) {

				uvs.push(x, y);

			}

			function checkTriangleVisibility(v1, v2, v3) {

				if (v1.visible === true || v2.visible === true || v3.visible === true) return true;

				_points3[0] = v1.positionScreen;
				_points3[1] = v2.positionScreen;
				_points3[2] = v3.positionScreen;

				return _clipBox.intersectsBox(_boundingBox.setFromPoints(_points3));

			}

			function checkBackfaceCulling(v1, v2, v3) {

				return ((v3.positionScreen.x - v1.positionScreen.x) *
					(v2.positionScreen.y - v1.positionScreen.y) -
					(v3.positionScreen.y - v1.positionScreen.y) *
					(v2.positionScreen.x - v1.positionScreen.x)) < 0;

			}

			function pushLine(a, b) {

				const v1 = _vertexPool[a];
				const v2 = _vertexPool[b];

				// Clip

				v1.positionScreen.copy(v1.position).applyMatrix4(_modelViewProjectionMatrix);
				v2.positionScreen.copy(v2.position).applyMatrix4(_modelViewProjectionMatrix);

				if (clipLine(v1.positionScreen, v2.positionScreen) === true) {

					// Perform the perspective divide
					v1.positionScreen.multiplyScalar(1 / v1.positionScreen.w);
					v2.positionScreen.multiplyScalar(1 / v2.positionScreen.w);

					_line = getNextLineInPool();
					_line.id = object.id;
					_line.v1.copy(v1);
					_line.v2.copy(v2);
					_line.z = Math.max(v1.positionScreen.z, v2.positionScreen.z);
					_line.renderOrder = object.renderOrder;

					_line.material = object.material;

					if (object.material.vertexColors) {

						_line.vertexColors[0].fromArray(colors, a * 3);
						_line.vertexColors[1].fromArray(colors, b * 3);

					}

					_renderData.elements.push(_line);

				}

			}

			function pushTriangle(a, b, c, material) {

				const v1 = _vertexPool[a];
				const v2 = _vertexPool[b];
				const v3 = _vertexPool[c];

				if (checkTriangleVisibility(v1, v2, v3) === false) return;

				if (material.side === DoubleSide || checkBackfaceCulling(v1, v2, v3) === true) {

					_face = getNextFaceInPool();

					_face.id = object.id;
					_face.v1.copy(v1);
					_face.v2.copy(v2);
					_face.v3.copy(v3);
					_face.z = (v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z) / 3;
					_face.renderOrder = object.renderOrder;

					// face normal
					_vector3.subVectors(v3.position, v2.position);
					_vector4.subVectors(v1.position, v2.position);
					_vector3.cross(_vector4);
					_face.normalModel.copy(_vector3);
					_face.normalModel.applyMatrix3(normalMatrix).normalize();

					for (let i = 0; i < 3; i++) {

						const normal = _face.vertexNormalsModel[i];
						normal.fromArray(normals, arguments[i] * 3);
						normal.applyMatrix3(normalMatrix).normalize();

						const uv = _face.uvs[i];
						uv.fromArray(uvs, arguments[i] * 2);

					}

					_face.vertexNormalsLength = 3;

					_face.material = material;

					if (material.vertexColors) {

						_face.color.fromArray(colors, a * 3);

					}

					_renderData.elements.push(_face);

				}

			}

			return {
				setObject: setObject,
				projectVertex: projectVertex,
				checkTriangleVisibility: checkTriangleVisibility,
				checkBackfaceCulling: checkBackfaceCulling,
				pushVertex: pushVertex,
				pushNormal: pushNormal,
				pushColor: pushColor,
				pushUv: pushUv,
				pushLine: pushLine,
				pushTriangle: pushTriangle
			};

		}

		const renderList = new RenderList();

		function projectObject(object) {

			if (object.visible === false) return;

			if (object.isLight) {

				_renderData.lights.push(object);

			} else if (object.isMesh || object.isLine || object.isPoints) {

				if (object.material.visible === false) return;
				if (object.frustumCulled === true && _frustum.intersectsObject(object) === false) return;

				addObject(object);

			} else if (object.isSprite) {

				if (object.material.visible === false) return;
				if (object.frustumCulled === true && _frustum.intersectsSprite(object) === false) return;

				addObject(object);

			}

			const children = object.children;

			for (let i = 0, l = children.length; i < l; i++) {

				projectObject(children[i]);

			}

		}

		function addObject(object) {

			_object = getNextObjectInPool();
			_object.id = object.id;
			_object.object = object;

			_vector3.setFromMatrixPosition(object.matrixWorld);
			_vector3.applyMatrix4(_viewProjectionMatrix);
			_object.z = _vector3.z;
			_object.renderOrder = object.renderOrder;

			_renderData.objects.push(_object);

		}

		this.projectScene = function (scene, camera, sortObjects, sortElements) {

			_faceCount = 0;
			_lineCount = 0;
			_spriteCount = 0;

			_renderData.elements.length = 0;

			if (scene.matrixWorldAutoUpdate === true) scene.updateMatrixWorld();
			if (camera.parent === null && camera.matrixWorldAutoUpdate === true) camera.updateMatrixWorld();

			_viewMatrix.copy(camera.matrixWorldInverse);
			_viewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, _viewMatrix);

			_frustum.setFromProjectionMatrix(_viewProjectionMatrix);

			//

			_objectCount = 0;

			_renderData.objects.length = 0;
			_renderData.lights.length = 0;

			projectObject(scene);

			if (sortObjects === true) {

				_renderData.objects.sort(painterSort);

			}

			//

			const objects = _renderData.objects;

			for (let o = 0, ol = objects.length; o < ol; o++) {

				const object = objects[o].object;
				const geometry = object.geometry;

				renderList.setObject(object);

				_modelMatrix = object.matrixWorld;

				_vertexCount = 0;

				if (object.isMesh) {

					let material = object.material;

					const isMultiMaterial = Array.isArray(material);

					const attributes = geometry.attributes;
					const groups = geometry.groups;

					if (attributes.position === undefined) continue;

					const positions = attributes.position.array;

					for (let i = 0, l = positions.length; i < l; i += 3) {

						let x = positions[i];
						let y = positions[i + 1];
						let z = positions[i + 2];

						const morphTargets = geometry.morphAttributes.position;

						if (morphTargets !== undefined) {

							const morphTargetsRelative = geometry.morphTargetsRelative;
							const morphInfluences = object.morphTargetInfluences;

							for (let t = 0, tl = morphTargets.length; t < tl; t++) {

								const influence = morphInfluences[t];

								if (influence === 0) continue;

								const target = morphTargets[t];

								if (morphTargetsRelative) {

									x += target.getX(i / 3) * influence;
									y += target.getY(i / 3) * influence;
									z += target.getZ(i / 3) * influence;

								} else {

									x += (target.getX(i / 3) - positions[i]) * influence;
									y += (target.getY(i / 3) - positions[i + 1]) * influence;
									z += (target.getZ(i / 3) - positions[i + 2]) * influence;

								}

							}

						}

						renderList.pushVertex(x, y, z);

					}

					if (attributes.normal !== undefined) {

						const normals = attributes.normal.array;

						for (let i = 0, l = normals.length; i < l; i += 3) {

							renderList.pushNormal(normals[i], normals[i + 1], normals[i + 2]);

						}

					}

					if (attributes.color !== undefined) {

						const colors = attributes.color.array;

						for (let i = 0, l = colors.length; i < l; i += 3) {

							renderList.pushColor(colors[i], colors[i + 1], colors[i + 2]);

						}

					}

					if (attributes.uv !== undefined) {

						const uvs = attributes.uv.array;

						for (let i = 0, l = uvs.length; i < l; i += 2) {

							renderList.pushUv(uvs[i], uvs[i + 1]);

						}

					}

					if (geometry.index !== null) {

						const indices = geometry.index.array;

						if (groups.length > 0) {

							for (let g = 0; g < groups.length; g++) {

								const group = groups[g];

								material = isMultiMaterial === true
									? object.material[group.materialIndex]
									: object.material;

								if (material === undefined) continue;

								for (let i = group.start, l = group.start + group.count; i < l; i += 3) {

									renderList.pushTriangle(indices[i], indices[i + 1], indices[i + 2], material);

								}

							}

						} else {

							for (let i = 0, l = indices.length; i < l; i += 3) {

								renderList.pushTriangle(indices[i], indices[i + 1], indices[i + 2], material);

							}

						}

					} else {

						if (groups.length > 0) {

							for (let g = 0; g < groups.length; g++) {

								const group = groups[g];

								material = isMultiMaterial === true
									? object.material[group.materialIndex]
									: object.material;

								if (material === undefined) continue;

								for (let i = group.start, l = group.start + group.count; i < l; i += 3) {

									renderList.pushTriangle(i, i + 1, i + 2, material);

								}

							}

						} else {

							for (let i = 0, l = positions.length / 3; i < l; i += 3) {

								renderList.pushTriangle(i, i + 1, i + 2, material);

							}

						}

					}

				} else if (object.isLine) {

					_modelViewProjectionMatrix.multiplyMatrices(_viewProjectionMatrix, _modelMatrix);

					const attributes = geometry.attributes;

					if (attributes.position !== undefined) {

						const positions = attributes.position.array;

						for (let i = 0, l = positions.length; i < l; i += 3) {

							renderList.pushVertex(positions[i], positions[i + 1], positions[i + 2]);

						}

						if (attributes.color !== undefined) {

							const colors = attributes.color.array;

							for (let i = 0, l = colors.length; i < l; i += 3) {

								renderList.pushColor(colors[i], colors[i + 1], colors[i + 2]);

							}

						}

						if (geometry.index !== null) {

							const indices = geometry.index.array;

							for (let i = 0, l = indices.length; i < l; i += 2) {

								renderList.pushLine(indices[i], indices[i + 1]);

							}

						} else {

							const step = object.isLineSegments ? 2 : 1;

							for (let i = 0, l = (positions.length / 3) - 1; i < l; i += step) {

								renderList.pushLine(i, i + 1);

							}

						}

					}

				} else if (object.isPoints) {

					_modelViewProjectionMatrix.multiplyMatrices(_viewProjectionMatrix, _modelMatrix);

					const attributes = geometry.attributes;

					if (attributes.position !== undefined) {

						const positions = attributes.position.array;

						for (let i = 0, l = positions.length; i < l; i += 3) {

							_vector4.set(positions[i], positions[i + 1], positions[i + 2], 1);
							_vector4.applyMatrix4(_modelViewProjectionMatrix);

							pushPoint(_vector4, object, camera);

						}

					}

				} else if (object.isSprite) {

					object.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, object.matrixWorld);
					_vector4.set(_modelMatrix.elements[12], _modelMatrix.elements[13], _modelMatrix.elements[14], 1);
					_vector4.applyMatrix4(_viewProjectionMatrix);

					pushPoint(_vector4, object, camera);

				}

			}

			if (sortElements === true) {

				_renderData.elements.sort(painterSort);

			}

			return _renderData;

		};

		function pushPoint(_vector4, object, camera) {

			const invW = 1 / _vector4.w;

			_vector4.z *= invW;

			if (_vector4.z >= - 1 && _vector4.z <= 1) {

				_sprite = getNextSpriteInPool();
				_sprite.id = object.id;
				_sprite.x = _vector4.x * invW;
				_sprite.y = _vector4.y * invW;
				_sprite.z = _vector4.z;
				_sprite.renderOrder = object.renderOrder;
				_sprite.object = object;

				_sprite.rotation = object.rotation;

				_sprite.scale.x = object.scale.x * Math.abs(_sprite.x - (_vector4.x + camera.projectionMatrix.elements[0]) / (_vector4.w + camera.projectionMatrix.elements[12]));
				_sprite.scale.y = object.scale.y * Math.abs(_sprite.y - (_vector4.y + camera.projectionMatrix.elements[5]) / (_vector4.w + camera.projectionMatrix.elements[13]));

				_sprite.material = object.material;

				_renderData.elements.push(_sprite);

			}

		}

		// Pools

		function getNextObjectInPool() {

			if (_objectCount === _objectPoolLength) {

				const object = new RenderableObject();
				_objectPool.push(object);
				_objectPoolLength++;
				_objectCount++;
				return object;

			}

			return _objectPool[_objectCount++];

		}

		function getNextVertexInPool() {

			if (_vertexCount === _vertexPoolLength) {

				const vertex = new RenderableVertex();
				_vertexPool.push(vertex);
				_vertexPoolLength++;
				_vertexCount++;
				return vertex;

			}

			return _vertexPool[_vertexCount++];

		}

		function getNextFaceInPool() {

			if (_faceCount === _facePoolLength) {

				const face = new RenderableFace();
				_facePool.push(face);
				_facePoolLength++;
				_faceCount++;
				return face;

			}

			return _facePool[_faceCount++];


		}

		function getNextLineInPool() {

			if (_lineCount === _linePoolLength) {

				const line = new RenderableLine();
				_linePool.push(line);
				_linePoolLength++;
				_lineCount++;
				return line;

			}

			return _linePool[_lineCount++];

		}

		function getNextSpriteInPool() {

			if (_spriteCount === _spritePoolLength) {

				const sprite = new RenderableSprite();
				_spritePool.push(sprite);
				_spritePoolLength++;
				_spriteCount++;
				return sprite;

			}

			return _spritePool[_spriteCount++];

		}

		//

		function painterSort(a, b) {

			if (a.renderOrder !== b.renderOrder) {

				return a.renderOrder - b.renderOrder;

			} else if (a.z !== b.z) {

				return b.z - a.z;

			} else if (a.id !== b.id) {

				return a.id - b.id;

			} else {

				return 0;

			}

		}

		function clipLine(s1, s2) {

			let alpha1 = 0, alpha2 = 1;

			// Calculate the boundary coordinate of each vertex for the near and far clip planes,
			// Z = -1 and Z = +1, respectively.

			const bc1near = s1.z + s1.w,
				bc2near = s2.z + s2.w,
				bc1far = - s1.z + s1.w,
				bc2far = - s2.z + s2.w;

			if (bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0) {

				// Both vertices lie entirely within all clip planes.
				return true;

			} else if ((bc1near < 0 && bc2near < 0) || (bc1far < 0 && bc2far < 0)) {

				// Both vertices lie entirely outside one of the clip planes.
				return false;

			} else {

				// The line segment spans at least one clip plane.

				if (bc1near < 0) {

					// v1 lies outside the near plane, v2 inside
					alpha1 = Math.max(alpha1, bc1near / (bc1near - bc2near));

				} else if (bc2near < 0) {

					// v2 lies outside the near plane, v1 inside
					alpha2 = Math.min(alpha2, bc1near / (bc1near - bc2near));

				}

				if (bc1far < 0) {

					// v1 lies outside the far plane, v2 inside
					alpha1 = Math.max(alpha1, bc1far / (bc1far - bc2far));

				} else if (bc2far < 0) {

					// v2 lies outside the far plane, v2 inside
					alpha2 = Math.min(alpha2, bc1far / (bc1far - bc2far));

				}

				if (alpha2 < alpha1) {

					// The line segment spans two boundaries, but is outside both of them.
					// (This can't happen when we're only clipping against just near/far but good
					//  to leave the check here for future usage if other clip planes are added.)
					return false;

				} else {

					// Update the s1 and s2 vertices to match the clipped line segment.
					s1.lerp(s2, alpha1);
					s2.lerp(s1, 1 - alpha2);

					return true;

				}

			}

		}

	}

}

export { RenderableObject, RenderableFace, RenderableVertex, RenderableLine, RenderableSprite, Projector };
